/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const { ConcatSource, RawSource } = require("webpack-sources");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const { getUndoPath } = require("../util/identifier");
const {
	getChunkFilenameTemplate,
	getCompilationHooks
} = require("./JavascriptModulesPlugin");
const {
	generateEntryStartup,
	updateHashForEntryStartup
} = require("./StartupHelpers");

/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Entrypoint")} Entrypoint */

const PLUGIN_NAME = "CommonJsChunkFormatPlugin";

class CommonJsChunkFormatPlugin {
	/**
	 * Apply the plugin
	 * @param {Compiler} compiler the compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		compiler.hooks.thisCompilation.tap(PLUGIN_NAME, compilation => {
			compilation.hooks.additionalChunkRuntimeRequirements.tap(
				PLUGIN_NAME,
				(chunk, set, { chunkGraph }) => {
					if (chunk.hasRuntime()) return;
					if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
						set.add(RuntimeGlobals.require);
						set.add(RuntimeGlobals.startupEntrypoint);
						set.add(RuntimeGlobals.externalInstallChunk);
					}
				}
			);
			const hooks = getCompilationHooks(compilation);
			hooks.renderChunk.tap(PLUGIN_NAME, (modules, renderContext) => {
				const { chunk, chunkGraph, runtimeTemplate } = renderContext;
				const source = new ConcatSource();
				source.add(`exports.id = ${JSON.stringify(chunk.id)};\n`);
				source.add(`exports.ids = ${JSON.stringify(chunk.ids)};\n`);
				source.add("exports.modules = ");
				source.add(modules);
				source.add(";\n");
				const runtimeModules = chunkGraph.getChunkRuntimeModulesInOrder(chunk);
				if (runtimeModules.length > 0) {
					source.add("exports.runtime =\n");
					source.add(
						Template.renderChunkRuntimeModules(runtimeModules, renderContext)
					);
				}
				const entries = Array.from(
					chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
				);
				if (entries.length > 0) {
					const runtimeChunk =
						/** @type {Entrypoint} */
						(entries[0][1]).getRuntimeChunk();
					const currentOutputName = compilation
						.getPath(
							getChunkFilenameTemplate(chunk, compilation.outputOptions),
							{
								chunk,
								contentHashType: "javascript"
							}
						)
						.replace(/^\/+/g, "")
						.split("/");
					const runtimeOutputName = compilation
						.getPath(
							getChunkFilenameTemplate(
								/** @type {Chunk} */
								(runtimeChunk),
								compilation.outputOptions
							),
							{
								chunk: /** @type {Chunk} */ (runtimeChunk),
								contentHashType: "javascript"
							}
						)
						.replace(/^\/+/g, "")
						.split("/");

					// remove common parts
					while (
						currentOutputName.length > 1 &&
						runtimeOutputName.length > 1 &&
						currentOutputName[0] === runtimeOutputName[0]
					) {
						currentOutputName.shift();
						runtimeOutputName.shift();
					}
					const last = runtimeOutputName.join("/");
					// create final path
					const runtimePath =
						getUndoPath(currentOutputName.join("/"), last, true) + last;

					const entrySource = new ConcatSource();
					entrySource.add(
						`(${
							runtimeTemplate.supportsArrowFunction() ? "() => " : "function() "
						}{\n`
					);
					entrySource.add("var exports = {};\n");
					entrySource.add(source);
					entrySource.add(";\n\n// load runtime\n");
					entrySource.add(
						`var ${RuntimeGlobals.require} = require(${JSON.stringify(
							runtimePath
						)});\n`
					);
					entrySource.add(`${RuntimeGlobals.externalInstallChunk}(exports);\n`);
					const startupSource = new RawSource(
						generateEntryStartup(
							chunkGraph,
							runtimeTemplate,
							entries,
							chunk,
							false
						)
					);
					entrySource.add(
						hooks.renderStartup.call(
							startupSource,
							entries[entries.length - 1][0],
							{
								...renderContext,
								inlined: false
							}
						)
					);
					entrySource.add("\n})()");
					return entrySource;
				}
				return source;
			});
			hooks.chunkHash.tap(PLUGIN_NAME, (chunk, hash, { chunkGraph }) => {
				if (chunk.hasRuntime()) return;
				hash.update(PLUGIN_NAME);
				hash.update("1");
				const entries = Array.from(
					chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)
				);
				updateHashForEntryStartup(hash, chunkGraph, entries, chunk);
			});
		});
	}
}

module.exports = CommonJsChunkFormatPlugin;
